﻿//almost bellow code from http://bcl.codeplex.com/wikipage (project "Long Path")
//code signature -> Copyright (c) Microsoft Corporation.  All rights reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using FILETIME = System.Runtime.InteropServices.ComTypes.FILETIME;

namespace McopyAPI
{
    internal class APIFunction
    {
        //Native function
        [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        [return: MarshalAs(UnmanagedType.Bool)]
        internal static extern bool CopyFile(string src, string dst, [MarshalAs(UnmanagedType.Bool)]bool failIfExists);

        [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        internal static extern SafeFindHandle FindFirstFile(string lpFileName, out WIN32_FIND_DATA lpFindFileData);

        [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        [return: MarshalAs(UnmanagedType.Bool)]
        internal static extern bool FindNextFile(SafeFindHandle hFindFile, out WIN32_FIND_DATA lpFindFileData);

        [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        [return: MarshalAs(UnmanagedType.Bool)]
        internal static extern bool CreateDirectory(string lpPathName,
           IntPtr lpSecurityAttributes);

        [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        internal static extern FileAttributes GetFileAttributes(string lpFileName);

        [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        internal static extern uint GetFullPathName(string lpFileName, uint nBufferLength,
           StringBuilder lpBuffer, IntPtr mustBeNull);

        [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        internal static extern int FormatMessage(int dwFlags, IntPtr lpSource, int dwMessageId, int dwLanguageId, StringBuilder lpBuffer, int nSize, IntPtr va_list_arguments);

        [DllImport("kernel32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        internal static extern bool FindClose(IntPtr hFindFile);

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
        internal struct WIN32_FIND_DATA
        {
            internal FileAttributes dwFileAttributes;
            internal FILETIME ftCreationTime;
            internal FILETIME ftLastAccessTime;
            internal FILETIME ftLastWriteTime;
            internal int nFileSizeHigh;
            internal int nFileSizeLow;
            internal int dwReserved0;
            internal int dwReserved1;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAX_PATH)]
            internal string cFileName;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAX_ALTERNATE)]
            internal string cAlternate;
        }

        internal const int ERROR_FILE_NOT_FOUND = 0x2;
        internal const int ERROR_PATH_NOT_FOUND = 0x3;
        internal const int ERROR_ACCESS_DENIED = 0x5;
        internal const int ERROR_INVALID_DRIVE = 0xf;
        internal const int ERROR_NO_MORE_FILES = 0x12;
        internal const int ERROR_INVALID_NAME = 0x7B;
        internal const int ERROR_ALREADY_EXISTS = 0xB7;
        internal const int ERROR_FILENAME_EXCED_RANGE = 0xCE;
        internal const int ERROR_DIRECTORY = 0x10B;
        internal const int ERROR_OPERATION_ABORTED = 0x3e3;
        internal const int INVALID_FILE_ATTRIBUTES = -1;
        internal const int MAX_PATH = 260;
        internal const int MAX_LONG_PATH = 32000;
        internal const int MAX_ALTERNATE = 14;
        internal const string LongPathPrefix = @"\\?\";
        internal const string LongPathNetworkPrefix = @"\\?\UNC\";
        public const int FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200;
        public const int FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000;
        public const int FORMAT_MESSAGE_ARGUMENT_ARRAY = 0x00002000;

        internal static WIN32_FIND_DATA GetFileHandle(string normalizedPathWithSearchPattern)
        {
            WIN32_FIND_DATA handle;
            BeginFind(normalizedPathWithSearchPattern, out handle);
            return handle;
        }

        private static IEnumerable<string> EnumerateDirSystemIterator(string normalizedPath, string normalizedSearchPattern, bool includeDirectories, bool includeFiles)
        {
            string path = RemoveLongPathPrefix(normalizedPath);

            WIN32_FIND_DATA findData;
            using (var handle = BeginFind(Path.Combine(normalizedPath, normalizedSearchPattern), out findData))
            {
                if (handle == null)
                    yield break;

                do
                {
                    string currentDirName = findData.cFileName;

                    if (IsDirectory(findData.dwFileAttributes))
                    {
                        if (includeDirectories && !IsCurrentOrParentDirectory(currentDirName))
                        {
                            yield return Path.Combine(path, currentDirName);
                        }
                    }
                    else
                    {
                        if (includeFiles)
                        {
                            yield return Path.Combine(path, currentDirName);
                        }
                    }
                } while (FindNextFile(handle, out findData));

                int errorCode = Marshal.GetLastWin32Error();
                if (errorCode != ERROR_NO_MORE_FILES)
                    throw GetExceptionFromWin32Error(errorCode);
            }
        }

        public static IEnumerable<string> EnumerateFiles(string path)
        {
            return EnumerateFiles(path, (string)null);
        }

        public static IEnumerable<string> EnumerateFiles(string path, string searchPattern)
        {
            return EnumerateFileSystemEntries(path, searchPattern, includeDirectories: false, includeFiles: true);
        }

        public static IEnumerable<string> EnumerateDirectories(string path)
        {
            return EnumerateDirectories(path, (string)null);
        }

        public static IEnumerable<string> EnumerateDirectories(string path, string searchPattern)
        {
            return EnumerateFileSystemEntries(path, searchPattern, includeDirectories: true, includeFiles: false);
        }

        private static IEnumerable<string> EnumerateFileSystemEntries(string path, string searchPattern, bool includeDirectories, bool includeFiles)
        {
            string normalizedSearchPattern = NormalizeSearchPattern(searchPattern);
            string normalizedPath = NormalizeLongPath(path);

            FileAttributes attributes;
            int errorCode = TryGetDirectoryAttributes(normalizedPath, out attributes);
            if (errorCode != 0)
            {
                throw GetExceptionFromWin32Error(errorCode);
            }

            return EnumerateFileSystemIterator(normalizedPath, normalizedSearchPattern, includeDirectories, includeFiles);
        }       

        private static IEnumerable<string> EnumerateFileSystemIterator(string normalizedPath, string normalizedSearchPattern, bool includeDirectories, bool includeFiles)
        {
            string path = RemoveLongPathPrefix(normalizedPath);

            WIN32_FIND_DATA findData;
            using (var handle = BeginFind(Path.Combine(normalizedPath, normalizedSearchPattern), out findData))
            {
                if (handle == null)
                    yield break;

                do
                {
                    string currentFileName = findData.cFileName;

                    if (IsDirectory(findData.dwFileAttributes))
                    {
                        if (includeDirectories && !IsCurrentOrParentDirectory(currentFileName))
                        {
                            path = NormalizeNetworkPath(path);
                            yield return Path.Combine(path, currentFileName);
                        }
                    }
                    else
                    {
                        if (includeFiles)
                        {
                            path = NormalizeNetworkPath(path);
                            yield return Path.Combine(path, currentFileName);
                        }
                    }
                } while (FindNextFile(handle, out findData));

                int errorCode = Marshal.GetLastWin32Error();
                if (errorCode != ERROR_NO_MORE_FILES)
                    throw GetExceptionFromWin32Error(errorCode);
            }
        }

        private static string NormalizeNetworkPath(string path)
        {
            if (String.Compare(path.Substring(0, 3), "UNC", false) != 0)
                return path;

            path = path.Substring(3, path.Length - 3);
            path = @"\" + path;
            return path;
        }

        public static bool IsSourceFileNewerThanDestinationFile(string sourceFile, string destinationFile)
        {
            //source file
            System.Runtime.InteropServices.ComTypes.FILETIME timeSourceLastWrite;
            var fileSourceProperties = GetFileHandle(ConvertToLongPath(sourceFile));
            timeSourceLastWrite = fileSourceProperties.ftLastWriteTime;

            //destination file
            System.Runtime.InteropServices.ComTypes.FILETIME timeDestinationLastWrite;
            var fileDestinationProperties = GetFileHandle(ConvertToLongPath(destinationFile));
            timeDestinationLastWrite = fileDestinationProperties.ftLastWriteTime;

            return timeSourceLastWrite.dwHighDateTime > timeDestinationLastWrite.dwHighDateTime ? true : false;
        }

        public static bool ExistsDirectory(string path)
        {
            bool isDirectory;
            if (Exists(path, out isDirectory))
            {
                return isDirectory;
            }
            return false;
        }

        public static bool ExistsFile(string path)
        {
            bool isDirectory;
            if (Exists(path, out isDirectory))
            {
                return !isDirectory;
            }
            return false;
        }

        internal static string RemoveLongPathPrefix(string normalizedPath)
        {
            return normalizedPath.Substring(LongPathPrefix.Length);
        }

        private static SafeFindHandle BeginFind(string normalizedPathWithSearchPattern, out WIN32_FIND_DATA findData)
        {
            var handle = FindFirstFile(normalizedPathWithSearchPattern, out findData);
            if (handle.IsInvalid)
            {
                int errorCode = Marshal.GetLastWin32Error();
                if (errorCode != ERROR_FILE_NOT_FOUND)
                    throw GetExceptionFromWin32Error(errorCode);

                return null;
            }

            return handle;
        }

        private static bool IsCurrentOrParentDirectory(string directoryName)
        {
            return directoryName.Equals(".", StringComparison.OrdinalIgnoreCase) || directoryName.Equals("..", StringComparison.OrdinalIgnoreCase);
        }

        internal static int TryGetDirectoryAttributes(string normalizedPath, out FileAttributes attributes)
        {
            int errorCode = TryGetFileAttributes(normalizedPath, out attributes);
            if (!IsDirectory(attributes))
                errorCode = ERROR_DIRECTORY;

            return errorCode;
        }

        internal static string NormalizeSearchPattern(string searchPattern)
        {
            if (String.IsNullOrEmpty(searchPattern) || String.Compare(searchPattern, ".", false) == 0)
                return "*";

            return searchPattern;
        }

        public static void Create(string path)
        {
            string normalizedPath = ConvertToLongPath(path);

            if (!CreateDirectory(normalizedPath, IntPtr.Zero))
            {
                int errorCode = Marshal.GetLastWin32Error();
                if (errorCode != ERROR_ALREADY_EXISTS || !ExistsDirectory(path))
                {
                    throw GetExceptionFromWin32Error(errorCode);
                }
            }
        }

        internal static bool Exists(string path, out bool isDirectory)
        {
            string normalizedPath;
            if (TryNormalizeLongPath(path, out normalizedPath))
            {
                FileAttributes attributes;
                int errorCode = TryGetFileAttributes(normalizedPath, out attributes);
                if (errorCode == 0)
                {
                    isDirectory = IsDirectory(attributes);
                    return true;
                }
            }
            isDirectory = false;
            return false;
        }

        internal static int TryGetFileAttributes(string normalizedPath, out FileAttributes attributes)
        {
            attributes = GetFileAttributes(normalizedPath);
            if ((int)attributes == INVALID_FILE_ATTRIBUTES)
                return Marshal.GetLastWin32Error();

            return 0;
        }

        internal static bool IsDirectory(FileAttributes attributes)
        {
            return (attributes & FileAttributes.Directory) == FileAttributes.Directory;
        }

        private static bool TryNormalizeLongPath(string path, out string result)
        {
            try
            {
                result = NormalizeLongPath(path);
                return true;
            }
            catch (ArgumentException)
            {
            }
            catch (PathTooLongException)
            {
            }
            result = null;
            return false;
        }

        internal static string NormalizeLongPath(string path)
        {
            return NormalizeLongPath(path, "path");
        }

        internal static string NormalizeLongPath(string path, string parameterName)
        {
            string output = string.Empty;

            if (path == null)
                throw new ArgumentNullException(parameterName);

            if (path.Length == 0)
                throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, "'{0}' cannot be an empty string.", parameterName), parameterName);

            uint length = (uint)path.Length;
            output = path;

            if (length == 0)
            {
                throw GetExceptionFromLastWin32Error(parameterName);
            }

            if (length > MAX_LONG_PATH)
            {
                throw GetExceptionFromWin32Error(ERROR_FILENAME_EXCED_RANGE, parameterName);
            }
            return ConvertToLongPath(output);
        }

        internal static string ConvertToLongPath(string inputPath)
        {
            string outputPath;
            //network share
            if (String.Compare(inputPath.Substring(0, 2), @"\\", false) == 0)
            {
                outputPath = inputPath.Substring(2, inputPath.Length - 2);
                outputPath = LongPathNetworkPrefix + outputPath;
            }
            //logical disc
            else
            {
                outputPath = LongPathPrefix + inputPath;
            }
            return outputPath;
        }

        internal static Exception GetExceptionFromLastWin32Error()
        {
            return GetExceptionFromLastWin32Error("path");
        }

        internal static Exception GetExceptionFromLastWin32Error(string parameterName)
        {
            return GetExceptionFromWin32Error(Marshal.GetLastWin32Error(), parameterName);
        }

        internal static Exception GetExceptionFromWin32Error(int errorCode)
        {
            return GetExceptionFromWin32Error(errorCode, "path");
        }

        internal static Exception GetExceptionFromWin32Error(int errorCode, string parameterName)
        {
            string message = GetMessageFromErrorCode(errorCode);

            switch (errorCode)
            {
                case ERROR_FILE_NOT_FOUND:
                    return new FileNotFoundException(message);

                case ERROR_PATH_NOT_FOUND:
                    return new DirectoryNotFoundException(message);

                case ERROR_ACCESS_DENIED:
                    return new UnauthorizedAccessException(message);

                case ERROR_FILENAME_EXCED_RANGE:
                    return new PathTooLongException(message);

                case ERROR_INVALID_DRIVE:
                    return new DriveNotFoundException(message);

                case ERROR_OPERATION_ABORTED:
                    return new OperationCanceledException(message);

                case ERROR_INVALID_NAME:
                    return new ArgumentException(message, parameterName);

                default:
                    return new IOException(message, MakeHRFromErrorCode(errorCode));
            }
        }

        private static string GetMessageFromErrorCode(int errorCode)
        {
            var buffer = new StringBuilder(512);

            int bufferLength = FormatMessage(FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ARGUMENT_ARRAY, IntPtr.Zero, errorCode, 0, buffer, buffer.Capacity, IntPtr.Zero);

            Contract.Assert(bufferLength != 0);

            return buffer.ToString();
        }

        internal static int MakeHRFromErrorCode(int errorCode)
        {
            return unchecked((int)0x80070000 | errorCode);
        }
    }
}